Skip to content

feat(slack-app phase 3a): /slack/events webhook + email-match identity#27

Merged
TS00 merged 1 commit intodevfrom
feat/slack-events-phase3a
Apr 28, 2026
Merged

feat(slack-app phase 3a): /slack/events webhook + email-match identity#27
TS00 merged 1 commit intodevfrom
feat/slack-events-phase3a

Conversation

@TS00
Copy link
Copy Markdown
Collaborator

@TS00 TS00 commented Apr 28, 2026

Summary

Phase 3a of the Slack app (full design: `docs/eng-plan-slack-app-v1.md`, parent memory `d959bc61`). Stands up the events webhook, signature verification, and the identity resolver. The bot now responds to @mentions in channels and DMs with a canned reply confirming who it sees you as. Phase 3b (next PR) swaps the canned reply for the Anthropic agent loop with memory tools.

What's new

  • `POST /slack/events` route — public (signature-verified, not Bearer-auth). preParsing hook captures the raw body so HMAC sees what Slack signed. URL verification echoes the challenge synchronously. `event_callback` acks 200 immediately and runs the handler as a fire-and-forget Promise (Slack's 3s deadline).
  • `src/slack-api.ts` — typed thin wrappers around `users.info`, `chat.postMessage`, `chat.postEphemeral`. `SlackResult` discriminated union for clean error handling.
  • `src/slack-identity.ts` — `resolveSlackUserToReflectUser`. One rule: the Slack user's email must lookup-match a Reflect user bound to the workspace's team (or the workspace's solo Reflect user). All refusal paths return a clean human-readable reason — no fallback link, no manual mapping UI (per Van's call earlier in the session).
  • `src/slack-events-handler.ts` — sync filter + async handler. Skips bot/edit/non-im events, posts ephemeral refusals in channels (don't shame the requester publicly) and regular replies in DMs. Audit-logs `slack.message.handled` or `slack.auth_refused` with both the Slack user ID and the resolved Reflect user ID.

Tests

347/347 backend tests green (was 335; +12). Coverage:

  • HTTP integration: URL verification, signature gate (missing/wrong/stale), event_callback ack-200 contract.
  • `processSlackEvent` unit: every ignored event-type path (bot_message, message_changed/deleted, public-channel non-mention, incomplete payloads, self-app messages).

End-to-end async handler tests with a mocked Slack are deferred to Phase 3b. Cross-process mocking (test process vs. server process) is fragile; manual smoke against the live `Reflect Dev` app is faster and more honest at this stage.

Manual step needed AFTER merge + dev deploy

The Slack app's Event Subscriptions are still OFF. After dev deploy lands:

  1. Open the `Reflect Dev` app at https://api.slack.com/apps
  2. Go to Event Subscriptions → toggle ON
  3. Set Request URL to `https://api-dev.reflectmemory.com/slack/events\`
  4. Slack will hit it for a URL verification challenge — should show ✓ Verified within seconds (this PR's code handles it)
  5. Under Subscribe to bot events, add: `app_mention` and `message.im`
  6. Save changes (Slack will prompt to reinstall the app to grant the new event scopes if needed)
  7. Add the bot to a channel: `/invite @reflect-dev`
  8. `@reflect-dev hi` — expect a canned reply confirming your email match

Test plan

  • CI green
  • Dev deploy succeeds, service boots cleanly
  • Event Subscriptions verification handshake passes in the Slack admin UI
  • @mention in a channel returns the Phase 3a reply ("Hi — I see you ()…")
  • DM the bot — same reply, but as a regular thread reply (not ephemeral)
  • @mention as a stranger (Slack user with email not in Reflect) → ephemeral refusal in channel; audit `slack.auth_refused` recorded

Made with Cursor

Stands up the Slack events endpoint, signature verification, and the
identity resolver. The bot now responds to @mentions in channels and DMs
with a Phase-3a canned reply confirming who it sees you as. Phase 3b
swaps the canned reply for the Anthropic agent loop with memory tools.

Backend:
- POST /slack/events route: public (signature-verified, not Bearer auth).
  preParsing hook captures the raw body so the HMAC sees the bytes Slack
  signed. URL verification handshake echoes the challenge synchronously.
  event_callback acks 200 immediately and runs the handler as a fire-
  and-forget Promise (Slack's 3s deadline).
- src/slack-api.ts: thin wrappers around users.info, chat.postMessage,
  chat.postEphemeral. SlackResult discriminated union for clean error
  handling.
- src/slack-identity.ts: resolveSlackUserToReflectUser. One rule: the
  Slack user's email must lookup-match a Reflect user that's bound to
  the workspace's team (or the workspace's solo Reflect user). All
  refusal paths return a clean human-readable reason — no fallback link,
  no manual mapping UI.
- src/slack-events-handler.ts: processSlackEvent (sync filter) +
  handleUserMessage (async). Skips bot/edit/non-im events, posts
  ephemeral refusals in channels and regular replies in DMs, records
  audit slack.message.handled or slack.auth_refused with both Slack
  user id and resolved Reflect user id.

Tests (+12, 347 total):
- HTTP integration: url_verification challenge echo, signature gate
  (missing/wrong/stale), event_callback ack-200 contract.
- processSlackEvent unit: ignored event types (bot_message,
  message_changed/deleted, public-channel non-mention, incomplete
  payloads, self-app messages).

Async handler tests against a real DB+mocked Slack are deferred to
Phase 3b — cross-process mocking is fragile; manual smoke against the
live Reflect Dev app is faster + more honest.

Refs: parent memory d959bc61.
Made-with: Cursor
@TS00 TS00 merged commit d19d53f into dev Apr 28, 2026
4 checks passed
@TS00 TS00 deleted the feat/slack-events-phase3a branch April 28, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant